# 46. socket模块 - 网络套接字模块

# 网络基础

这一篇,不是整个网络编程都需要网络知识

网络基础我就不记了,记录个网址,讲的就是网络编程,里面就有网络知识

地址:https://www.cnblogs.com/LY-C/p/9089331.html

# socket模块

# socket模块概念

socket套接字分成二种

  1. 基于文件类型的套接字:AF_UNIX
    1. 基于unix系统开发出来,继承unix系统的特性,一切皆为文件,所以基于文件类型的套接字就是调用底层的文件系统来取数据的,
    2. 两个套接字进程运行在同一个机器,可以通过访问同一个文件系统来间接的完全通信
    3. 目前不使用
  2. 基于网络类型的套接字:AF_INET
    1. 还有其他的地址套接被爆了,但是AF_INET套接字使用最为广泛的一个
    2. 在Python中支持很多地址套接字
    3. 还有一个AF_INET6,是用于IPV6的,目前无使用,还是要知道为好
    4. 目前就使用基于网络类型的套接字:AF_INET

# 网络传输协议

网络传输协议分为二种

  1. TCP协议
    1. TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序
  2. UDP协议
    1. UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)

# socket模块初试

socket.socket() ## 是有几个选项,接下来看一下常用选项

 def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):  ## 这是socket这个类的__init__方法

可以看出来:
family:使用什么套接字,默认使用AF_INET,网络类型套接字
type:使用什么协议,默认使用SOCK_STREAM,TCP协议
	TCP协议:SOCK_STREAM
	UDP协议:SOCK_DGRAM
proto:协议号通常为零,可以省略,或者在地址套接字为AF_CAN的情况下,协议应为CAN_RAW或CAN_RCM之一
fileno:如果指定了fileno,则其他参数将被忽略,使用带有指定文件描述符的套接字返回
	和socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的
	这可能有助于使用socket.close()关闭一个独立的插座

# socket模块的TCP用法

特点

  1. 发送时,需要进入连接服务器测试
  2. 一发一收
  3. 无法同时多台客户端聊天,只有等一台客户端停止断开连接,才能跟下一台客户端连接

# 编写能接收客户端的消息

## 服务端的代码 server.py
import socket
sk = socket.socket()  ## 实例化一个对象,类来自socket模块中
sk.bind(("192.168.0.90",18080))  ## 绑定IP地址跟端口,到这个程序中,必须是以元组类型
sk.listen()  ## 开机并监控
conn,addr = sk.accept()  ## 接收客户的连接
ret = conn.recv(1024) ## 接收客户端发来的消息,1024代表的是:接收不超过1024字节的数据
print(ret.decode()) ## 打印客户端发来的信息
conn.close()  ## 关闭链接
sk.close()  ##关闭socket服务


## 客户端的代码 client.py
import socket
sk = socket.socket() ## 实例化一个对象,类来自socket模块中
sk.connect(("192.168.0.90",18080))  ## 尝试连接服务器,服务器IP跟端口 必须是以元组类型
sk.send("你好".encode("utf-8"))  ## 发送消息,必须要用bytes类型的数据才能发送
sk.close() ## 关闭

执行顺序
先把服务端启动
在启动客户端

执行结果:
你好

# 简单完善"编写能接收客户端的消息"

## 服务端的代码 server.py
import socket
while 1:
    print("本程序支持所有场景输入 Q 退出")
    sk = socket.socket()  ## 实例化一个对象,类来自socket模块中
    sk.bind(("192.168.0.90",18080))  ## 绑定IP地址跟端口,到这个程序中,必须是以元组类型
    sk.listen()  ## 开机并监控
    conn,addr = sk.accept()  ## 接收客户的连接
    ret = conn.recv(1024) ## 接收客户端发来的消息,1024代表的是:接收不超过1024字节的数据
    print("==========客户端发来的消息==========")
    print(ret.decode()) ## 打印客户端发来的信息
    print("====================================")
    user = input("请输入你要回复的消息:")
    if user.upper() == "Q":
        exit()
    conn.send(user.encode("utf-8")) ## 回复客户端消息
    conn.close()  ## 关闭链接
    sk.close()  ##关闭socket服务


## 客户端的代码 client.py
import socket
while 1:
    print("本程序支持所有场景输入 Q 退出")
    sk = socket.socket() ## 实例化一个对象,类来自socket模块中
    sk.connect(("192.168.0.90",18080))  ## 尝试连接服务器,服务器IP跟端口 必须是以元组类型
    user = input("请输入你要发送的消息:")
    if user.upper() == "Q":
        exit()
    sk.send(user.encode("utf-8"))  ## 发送消息,必须要用bytes类型的数据才能发送
    ret = sk.recv(1024)  ## 接收服务端发来的消息,1024代表的是:接收不超过1024字节的数据
    print("==========服务端发来的消息==========")
    print(ret.decode("utf-8"))
    print("====================================")
    sk.close() ## 关闭

# 相对完善"编写能接收客户端的消息"

## 服务端的代码 server.py
import socket
sk = socket.socket()
sk.bind(('192.168.43.226',8090))
sk.listen()
while 1:
    conn,addr = sk.accept()
    while 1:
        ret = conn.recv(1024)
        res = ret.decode("utf-8")
        if ret.decode("utf-8").upper() == "Q":
            print("客户端退出")
            break
         	print("==========客户端发来的消息==========")
        print(ret.decode()) ## 打印客户端发来的信息
        print("====================================")
        user = input("请输入你要回复的消息:")
        conn.send(user.encode("utf-8"))
        if user.upper() == "Q":
            print("主动退出")
            break
    conn.close()
sk.close()


## 客户端的代码 client.py
import socket
sk = socket.socket()
sk.connect(('192.168.43.226',8090))
while 1:
    user = input("请输入你要发送的消息:")
    sk.send(user.encode("utf-8"))
    if user.upper() == "Q":
        print("主动退出")
        break
    ret = sk.recv(1024)
    print("==========服务端发来的消息==========")
    print(ret.decode())  ## 打印客户端发来的信息
    print("====================================")
    if ret.decode("utf-8").upper() == "Q":
        print("服务端退出")
        break
sk.close()

## 比较完善接收客户端消息并能回复的功能,还可以客户端退出断开连接后还能接收到另个客户端的连接请求

# socket模块的UDP用法

特点:

  1. 发送时,无需进行服务端测试
  2. 多发多收
  3. 可以同时进行多台客户端聊天
    1. 可以遵守以下的规则
      1. 一发一收
      2. 只收
      3. 只发

# 简单编写

## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
coon,addr = sk.recvfrom(1024) ## 接收括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息

## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
user = input("要发送的消息:") ## 要发送的消息
sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端

## 简单实现了使用UDP协议的接收跟发送功能

# 简单完善编写

## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
while 1:
    coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
    print(coon.decode("utf-8")) ## 输出接收到的消息
    user = input("请输入你要回复的信息:")
    sk.sendto(user.encode('utf-8'),addr)
sk.close()


## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
while 1:
    user = input("要发送的消息:") ## 要发送的消息
    sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端
    coon, addr = sk.recvfrom(1024)  ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
    print(coon.decode("utf-8"))  ## 输出接收到的消息
    
## 实现了让客户端跟服务器进行聊天的功能,实现了服务端能对多个客户端进行聊天

# 比较完善编写 - 客户端发消息带名字

## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
while 1:
    coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
    print(coon.decode("utf-8")) ## 输出接收到的消息
    user = input("请输入你要回复的信息:")
    sk.sendto(user.encode('utf-8'),addr)
sk.close()


## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
name = input("请输入你的用户名:")
while 1:
    user = input("要发送的消息:") ## 要发送的消息
    user = name + ":" + user
    sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端
    coon, addr = sk.recvfrom(1024)  ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
    print(coon.decode("utf-8"))  ## 输出接收到的消息

# 接收的消息带上颜色

## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
dic = {'江凡':'\033[32m',"李城":"\033[33m"}
while 1:
    coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
    msg_r = coon.decode('utf-8')
    name = msg_r.split(':')[0].strip()
    color = dic.get(name, '')  # get(key,default)  获取字典中key对应的value,如果没有key则返回default
    print('%socket %socket \033[0m' %(color,msg_r))
    user = input("请输入你要回复的信息:")
    sk.sendto(user.encode('utf-8'),addr)
sk.close()


## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)# udp协议
name = input('请输入您的名字:')
# 收发
while 1:
    msg_s = input(('>>>'))
    info = name + ' : ' + msg_s
    sk.sendto(info.encode('utf-8'), ('127.0.0.1',8090))  # 发给谁的消息
    msg_r,addr = sk.recvfrom(1024)# 接收来自于哪里的消息
    print(msg_r.decode('utf-8'))
sk.close()

# 减少socket使用时候的操作

需要自定义一个模块文件,如下

## 自定义模块,my_socket
import socket
class My_socket(socket.socket):
    def __init__(self,encoding="utf-8"):
        self.encoding = encoding
        super(My_socket,self).__init__(type=socket.SOCK_DGRAM)

    def se(self,coon,addr):
        return self.sendto(coon.encode(self.encoding),addr)

    def re(self,num):
        conn, addr = self.recvfrom(num)
        return conn.decode(self.encoding),addr
    
## 服务端的代码 server.py
import my_socket
sk = my_socket.My_socket()
sk.bind(('127.0.0.1',18080))
dic = {'江凡':'\033[32m',"李城":"\033[33m"}
while 1:
    coon,addr = sk.re(1024)
    print(coon)
    user = input("请输入你要回复的信息:")
    sk.se(user,addr)
sk.close()


## 客户端的代码 client.py
import my_socket
sk = my_socket.My_socket()
name = input("请输入你的用户名:")
while 1:
    user = input("要发送的消息:")
    user = name + ":" + user
    sk.se(user,('127.0.0.1',18080))
    coon, addr = sk.re(1024)
    print(coon)

# socket的其他用法

# 服务端套接字函数

方法 用法
socket.bind() 绑定(主机,端口号)到套接字
socket.listen() 开始TCP监听
socket.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

# 客户端套接字函数

方法 用法
socket.connect() 主动初始化TCP服务器连接
socket.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

# 公共用途的套接字函数

方法 用法
socket.recv() 接收TCP数据
socket.send() 发送TCP数据
socket.sendall() 发送TCP数据
socket.recvfrom() 接收UDP数据
socket.sendto() 发送UDP数据
socket.getpeername() 连接到当前套接字的远端的地址
socket.getsockname() 当前套接字的地址
socket.getsockopt() 返回指定套接字的参数
socket.setsockopt() 设置指定套接字的参数
socket.close() 关闭套接字

# 面向锁的套接字方法

方法 用法
socket.setblocking() 设置套接字的阻塞与非阻塞模式
socket.settimeout() 设置阻塞套接字操作的超时时间
socket.gettimeout() 得到阻塞套接字操作的超时时间

# 面向文件的套接字的函数

方法 用法
socket.fileno() 套接字的文件描述符
socket.makefile() 创建一个与该套接字相关的文件
socket.connect() 主动初始化TCP服务器连接
socket.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

# 公共用途的套接字函数

方法 用法
socket.recv() 接收TCP数据
socket.send() 发送TCP数据
socket.sendall() 发送TCP数据
socket.recvfrom() 接收UDP数据
socket.sendto() 发送UDP数据
socket.getpeername() 连接到当前套接字的远端的地址
socket.getsockname() 当前套接字的地址
socket.getsockopt() 返回指定套接字的参数
socket.setsockopt() 设置指定套接字的参数
socket.close() 关闭套接字

# 面向锁的套接字方法

方法 用法
socket.setblocking() 设置套接字的阻塞与非阻塞模式
socket.settimeout() 设置阻塞套接字操作的超时时间
socket.gettimeout() 得到阻塞套接字操作的超时时间

# 面向文件的套接字的函数

方法 用法
socket.fileno() 套接字的文件描述符
socket.makefile() 创建一个与该套接字相关的文件

# socketserver模块 - 解决TCP无法多人连接

解决TCP协议无法多人连接的模块,也是基于socket模块的基础上

# socketserver模块的固定格式

import socketserver

class Msocket(socketserver.BaseRequestHandler): # 除了类名能修改之类,父类不能改
    def handle(self):  # 方法名不能修改,但是方法下的逻辑可以自由修改
        # 收发的逻辑代码
        pass

server = socketserver.TCPServer(('127.0.0.1',8080),MySocket)    # 括号中的都可以修改,服务器的IP跟端口,还有创建的类的类名
server.serve_forever()  # 代表开启一个永久性的服务

# socketserver模块的简单使用

# 服务端的代码
import socketserver
class Msocket(socketserver.BaseRequestHandler): # 除了类名能修改之类,父类不能改
    def handle(self):  # 方法名不能修改,但是方法下的逻辑可以自由修改
        # 收发的逻辑代码
        msg = self.request.recv(1024).decode("utf-8")
        print(msg)

server = socketserver.TCPServer(('127.0.0.1',8080),Msocket)    # 括号中的都可以修改,服务器的IP跟端口,还有创建的类的类名
server.serve_forever()  # 代表开启一个永久性的服务

# 客户端的代码
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))

msg_s = input('>>>')
sk.send(msg_s.encode('utf-8'))

print(sk.recv(1024).decode('utf-8'))

sk.close()

说实话,我暂时也不知道这个模块有什么用,他只能让多人连接,但是不能让多少进行通信

这模块差不多就像是在TCP中创建一个对列,让连接的用户进行这个对列,程序处理的时候调用这个对列

就算使用了这个模块,TCP也只能进行一对一通信,只是连接可以连接到而已

感觉使用这个模块让TCP拥有UDP的一些特性功能

# 网络传输问题之TCP-粘包

什么是粘包,发送端发送数据,接收端不知道应该如何去接收,造成的一种数据混乱的现象

**注意:**粘包只会在使用TCP协议传输才会发生,使用UDP则不会发生

socker数据传输过程

  1. 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
  2. 也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
  3. 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
  4. 怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
  5. 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
  6. 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法 (opens new window)把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

TCP跟UDP发送数据长度的限制

  1. 用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)
  2. 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送

# 粘包发生的二种情况

在TCP协议传输中发生粘包的二种情况

  1. TCP协议的拆包机制
    1. 当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去。
      1. MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。
      2. MTU的单位是字节。 大部分网络设备的MTU都是1500。
      3. 如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
  2. 面向流的通信特点和Nagle算法
    1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
    2. 收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
    3. 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
    4. 可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。